Skip to content

fix: show aggregation errors in Chart.svelte instead of an infinite spinner - #9838

Open
eminemead wants to merge 1 commit into
rilldata:mainfrom
eminemead:xiaofei.yin/fix-chart-error-spinner
Open

eminemead wants to merge 1 commit into
rilldata:mainfrom
eminemead:xiaofei.yin/fix-chart-error-spinner

Conversation

@eminemead

Copy link
Copy Markdown
  • Failed aggregations left the chart on an infinite diamond spinner.
  • Chart.svelte checked isFetching before the query error, so a failed request never left the loading state.
  • Render the aggregation error first; keep the spinner only when there is no error.

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

Developed in collaboration with Claude Code

Chart.svelte checked isFetching before the query error, so a failed fill-missing aggregation never left the diamond.
@nishantmonu51 nishantmonu51 added Type:Bug Something isn't working Area:Dashboard Size:M Medium change: 100-499 lines labels Sep 1, 2026
@nishantmonu51
nishantmonu51 self-requested a review September 8, 2026 05:39
@nishantmonu51

Copy link
Copy Markdown
Collaborator

1. isFetching was not what swallowed the error; measures.length === 0 was, and that path still spins forever.

The query client caps retries (globalQueryClient.ts: retry only for network errors, max two), so once an aggregation fails, isFetching is false and the old {:else if error} branch was already reachable. The condition that actually keeps the chart on the spinner is measures.length === 0 in the same #if: getMeasuresForMetricView returns [] whenever the metrics view resource is missing or has no validSpec, and nothing ever changes that. The reorder does fix the reported bug, because a chart pointed at a missing or invalid metrics view both errors and has empty measures — but the residual case is worth handling: an empty measures with no error (metrics view absent, or still not in the resource list) is still an indefinite spinner. Consider distinguishing "measures not loaded yet" from "metrics view has no measures" rather than folding the latter into the loading branch.

2. The tests do not cover the scenario that produced the bug.

Every case passes measures = [measure], and the first case pairs isFetching: true with an error, a combination the retry configuration makes unlikely in practice. The regression this PR fixes is measures: [] plus an error, and that is the case worth pinning:

renderChart(chartData({ isFetching: false, error: new Error("...") }), []);

Also, expect(container.querySelector(".status")).toBeNull() passes vacuously if Spinner.svelte ever renames its class, so the absence assertions are weaker than they look.

3. The vitest-setup.ts localStorage mock appears unnecessary.

Chart.spec.ts passes without it on both Node 20 and Node 22 (npx -y node@22 ../node_modules/vitest/vitest.mjs run src/features/components/charts/Chart.spec.ts with the setup file reverted to main). The test.projects config declares only the jsdom environment, where jsdom supplies localStorage, so the guard's branch is not taken. If it is needed for a case I have not reproduced, the PR should say which one; otherwise dropping those 34 lines keeps an unrelated global change out of a targeted bug fix. If it stays, note that clearMocks: true does not reset the backing Map, so storage written by one test is visible to the next.

4. Minor: a stale error now suppresses the spinner on refetch.

With error checked first, a query that failed and is being refetched (a filter or time-range change after a failure) shows the previous error until the new response lands, where it used to show the spinner. That is a reasonable trade against an infinite spinner, but if the empty-measures case in point 1 is handled separately, isFetching && !error becomes unnecessary and the ordering question goes away.

@nishantmonu51 nishantmonu51 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The branch reorder is correct and fixes the reported symptom. Two comments below, on what the new spec does not cover and on the unrelated vitest-setup.ts change.

Comment on lines +44 to +55
describe("Chart loading and error states", () => {
it("shows the query error instead of the spinner", () => {
const { container, queryByText } = renderChart(
chartData({
isFetching: true,
error: new Error("time range has too many bins"),
}),
);

expect(queryByText("time range has too many bins")).toBeInTheDocument();
expect(container.querySelector(".status")).toBeNull();
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every case here passes measures = [measure], so the measures.length === 0 half of the loading branch, which is what actually kept the chart spinning, is never exercised. With a non-empty measures the old {:else if error} branch was already reachable: the query client only retries network errors (web-common/src/lib/svelte-query/globalQueryClient.ts:13-14), so a failed aggregation settles with isFetching: false and error set. The regression to pin is chartData({ isFetching: false, error }) with measures: [], which is the state of a chart pointed at a missing or invalid metrics view, since getMeasuresForMetricView returns [] whenever there is no validSpec (web-common/src/features/metrics-views/metrics-view-selectors.ts:100-105). The existing isFetching: true plus error case only covers refetch-after-failure.

Comment on lines +25 to +57
// Node 22+ exposes a global `localStorage` that is undefined unless
// --localstorage-file is set. Theme and other stores read the global at
// import time, so pin it to a memory-backed mock for jsdom tests.
if (
typeof globalThis.localStorage === "undefined" ||
typeof globalThis.localStorage?.getItem !== "function"
) {
const memory = new Map<string, string>();
const localStorageMock: Storage = {
get length() {
return memory.size;
},
clear: () => memory.clear(),
getItem: (key: string) => memory.get(key) ?? null,
key: (index: number) => [...memory.keys()][index] ?? null,
removeItem: (key: string) => {
memory.delete(key);
},
setItem: (key: string, value: string) => {
memory.set(key, value);
},
};
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
writable: true,
value: localStorageMock,
});
Object.defineProperty(window, "localStorage", {
configurable: true,
writable: true,
value: localStorageMock,
});
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shim is unrelated to the branch reorder and is inert on the pinned toolchain: .nvmrc is Node 22, where localStorage exists only behind --experimental-webstorage, and Chart.spec.ts passes on Node 20 with this file reverted to main. It is needed only on Node 25+, where localStorage is an unflagged global that is undefined without --localstorage-file and ThemeControl reads it at import time via explicitLocalStorageStore (web-common/src/features/themes/theme-control.ts:22-24), so the comment's claim about Node 22+ is inaccurate as written. If it stays, note that clearMocks: true does not reset the backing Map, so storage written by one test is visible to the next in the same file.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area:Dashboard Size:M Medium change: 100-499 lines Type:Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants